package com.db.hashmap;

import java.nio.ByteBuffer;
import java.nio.IntBuffer;

/**
 * This class represents realization of BufferHashMap for primitive type int with emulated chains of collisions
 * @author Alexander Kotov (kotov.alex.22@gmail.com)
 */
public class ByteBufferHashMap {

    /**
     * The number of key-value mappings contained in this map.
     */
    private int size;

    /**
     * The maximum number of key-value mappings which can be contained in this map.
     */
    private int capacity;

    private int firstFreeEntry;

    /**
     * The internal buffer.
     */
    private IntBuffer buffer;

    /**
     * Constructs an empty <tt>ByteBufferHashMap</tt> with collisions chains
     *
     * @param byteBuffer buffer is used for store map elements
     * @throws IllegalArgumentException if the byteBuffer has capacity less than 16
     */
    public ByteBufferHashMap(ByteBuffer byteBuffer) {
        buffer = byteBuffer.asIntBuffer();
        capacity = buffer.capacity() / 4;

        if (capacity == 0) {
            throw new IllegalArgumentException("Not enough capacity");
        }
        clear();
    }

    /**
     * Clear ByteBufferHashMap
     */
    public void clear() {
        buffer.position(0);
        for (int i = 0; i < buffer.capacity(); ++i) {
            buffer.put(-1);
        }
        size = 0;
        firstFreeEntry = 0;
    }

    /**
     * @param K specified key
     * @return the value to which the specified key is mapped,
     * or <tt>null</tt> if this map contains no mapping for the key.
     */
    public Integer get(int K) {
        final int index = getBucketIndex(K);
        for (int entry = getBucket(index); entry != -1; entry = getNextEntry(entry)) {
            if (K == getKey(entry)) {
                return getValue(entry);
            }
        }
        return null;
    }

    /**
     * Associates the specified value with the specified key in this map.
     * If the map previously contained a mapping for the key, the old
     * value is replaced.
     *
     * @param K key with which the specified value is to be associated
     * @param V value to be associated with the specified key
     * @return the previous value associated with <tt>K</tt>, or
     *         <tt>null</tt> if there was no mapping for <tt>K</tt>.
     * @throws OutOfCapacityException if there is no space to put the element
     */
    public Integer put(int K, int V) {
        final int bucketIndex = getBucketIndex(K);
        if (getBucket(bucketIndex) == -1) {
            incrementSize();
            final int freeEntry = getFirstFreeEntry();
            setBucket(bucketIndex, freeEntry);
            setKey(freeEntry, K);
            setValue(freeEntry, V);
            return null;
        }
        int index = getBucket(bucketIndex);
        for (int entry = index; entry != -1; index = entry, entry = getNextEntry(entry)) {
            if (K == getKey(entry)) {
                int result = getValue(entry);
                setValue(entry, V);
                return result;
            }
        }
        incrementSize();
        final int freeEntry = getFirstFreeEntry();
        setEntry(index, freeEntry);
        setKey(freeEntry, K);
        setValue(freeEntry, V);
        return null;
    }

    /**
     * Removes the mapping for a key from this map if it is present
     *
     * @param K key with which the specified element is to be removed
     * @return the value to which this map previously associated the <tt>K</tt>, or
     *         <tt>null</tt> if the map contained no mapping for the <tt>K</tt>.
     */
    public Integer remove(int K) {
        final int bucketIndex = getBucketIndex(K);
        if (getBucket(bucketIndex) == -1) {
            return null;
        }
        int chainLength = 0;
        int index = getBucket(bucketIndex);
        for (int entry = index; entry != -1; index = entry, entry = getNextEntry(entry), ++chainLength) {
            if (K == getKey(entry)) {
                int result = getValue(entry);

                if (chainLength == 0) {
                    setBucket(getBucketIndex(K), -1);
                }

                if (entry != capacity - 1) {
                    setEntry(index, getNextEntry(entry));
                } else {
                    setEntry(index, -1);
                }
                setEntry(entry, -1);
                firstFreeEntry = entry;
                size--;
                return result;
            }
        }
        return null;
    }

    /**
     * @return the number of key-value mappings contained in this map.
     */
    public int size() {
        return size;
    }

    /**
     *
     * @param K key
     * @return true if map contains element with key K,
     * false otherwise.
     */
    public boolean contains(int K) {
        final int hashCode = getBucketIndex(K);
        for (int entry = getBucket(hashCode); entry != -1; entry = getNextEntry(entry)) {
            if (K == getKey(entry)) {
                return true;
            }
        }
        return false;
    }

    private int getBucketIndex(int value) {
        return value % capacity;
    }

    private int getNextEntry(int entry) {
        return getEntry(entry);
    }

    private int getBucket(int index) {
        return buffer.get(index);
    }

    private int getEntry(int index) {
        return buffer.get(capacity + index);
    }

    private int getKey(int index) {
        return buffer.get(2 * capacity + index);
    }

    private int getValue(int index) {
        return buffer.get(3 * capacity + index);
    }

    private void setBucket(int index, int value) {
        buffer.put(index, value);
    }

    private void setEntry(int index, int value) {
        buffer.put(capacity + index, value);
    }

    private void setKey(int index, int value) {
        buffer.put(2 * capacity + index, value);
    }

    private void setValue(int index, int value) {
        buffer.put(3 * capacity + index, value);
    }

    private void incrementSize() {
        if (size == capacity) {
            throw new OutOfCapacityException("There is no space to put element.");
        }
        size++;
    }

    private int getFirstFreeEntry() {
        int oldValue = firstFreeEntry;
        while (firstFreeEntry < capacity && getEntry(++firstFreeEntry) != -1);
        return oldValue;
    }
}
